import os
import logging
from logging.handlers import RotatingFileHandler
from time import perf_counter
from typing import List
import sys
import threading
import time
import webbrowser

import anyio
from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse
import httpx

# 兼容直接运行脚本的场景：确保项目根目录在 sys.path 中
# 当以 “python app/main.py” 或绝对路径运行时，sys.path[0] 指向 app 目录，
# 会导致 “from app.schemas ...” 的绝对导入失败。将父目录加入 sys.path 以修复。
try:
    _current_dir = os.path.dirname(os.path.abspath(__file__))
    _project_root = os.path.dirname(_current_dir)
    if _project_root not in sys.path:
        sys.path.insert(0, _project_root)
except Exception:
    # 保守处理：若上述逻辑异常，继续按默认路径搜索
    pass

# 使用绝对导入，确保在打包后的环境与源码环境均可用
from app.schemas import DecodeResponse, PerFileResult, ImageInfo, BarcodeItem
from app.barcode import decode_barcodes, get_image_dimensions


def resource_path(relative_path):
    """获取资源的绝对路径，无论是从源代码运行还是作为打包的 exe 运行。"""
    try:
        # PyInstaller 创建一个临时文件夹并将路径存储在 _MEIPASS 中
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")

    return os.path.join(base_path, relative_path)


def setup_logging() -> logging.Logger:
    """
    函数说明：初始化应用日志记录器，支持滚动日志与控制台输出。

    返回：
    - 已配置好的Logger对象
    """
    os.makedirs("logs", exist_ok=True)
    logger = logging.getLogger("barcode_api")
    logger.setLevel(logging.INFO)

    # 文件滚动日志：写入到当前工作目录下的 logs，避免写入 PyInstaller 的临时目录
    log_dir = os.path.join(os.getcwd(), "logs")
    os.makedirs(log_dir, exist_ok=True)
    file_handler = RotatingFileHandler(
        filename=os.path.join(log_dir, "app.log"),
        maxBytes=1_000_000,
        backupCount=3,
        encoding="utf-8",
    )
    formatter = logging.Formatter(
        fmt="%(asctime)s %(levelname)s %(name)s: %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S",
    )
    file_handler.setFormatter(formatter)
    logger.addHandler(file_handler)

    # 控制台日志
    console_handler = logging.StreamHandler()
    console_handler.setFormatter(formatter)
    logger.addHandler(console_handler)

    return logger


logger = setup_logging()
app = FastAPI(
    title="一维码识别API",
    description="基于FastAPI与pyzbar的高性能一维码识别服务，支持PNG/JPG上传。",
    version="1.0.0",
)

# 跨域支持（可按需调整）
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 挂载前端静态页面（/ui 显示 index.html）
frontend_dir = resource_path("frontend")
if os.path.isdir(frontend_dir):
    app.mount("/ui", StaticFiles(directory=frontend_dir, html=True), name="ui")


@app.get("/healthz")
def healthz() -> dict:
    """
    函数说明：健康检查接口，用于判断服务是否可用。

    返回：
    - 字典：{"status": "ok"}
    """
    return {"status": "ok"}


@app.get("/", response_class=HTMLResponse)
def root_index() -> str:
    """
    函数说明：根路由，提供进入前端页面的入口链接。

    返回：
    - 简单HTML，包含跳转到 /ui 的链接
    """
    return (
        """
        <!doctype html>
        <html lang="zh-CN">
        <head>
          <meta charset="utf-8" />
          <meta name="viewport" content="width=device-width, initial-scale=1" />
          <title>一维码识别API</title>
          <style>body{font-family:system-ui,Segoe UI,Arial;margin:40px}a{color:#2563eb;text-decoration:none}</style>
        </head>
        <body>
          <h1>一维码识别API</h1>
          <p>前端上传与结果展示页面：<a href="/ui" target="_self">打开 /ui</a></p>
          <p>Swagger 文档：<a href="/docs" target="_self">/docs</a></p>
        </body>
        </html>
        """
    )


@app.post("/api/v1/barcode/decode", response_model=DecodeResponse)
async def decode_endpoint(files: List[UploadFile] = File(...)) -> DecodeResponse:
    """
    函数说明：上传图片并进行一维码识别，返回结构化结果。

    行为：
    - 接收多文件上传（PNG/JPG等常见格式）
    - 对每张图片进行一维码识别
    - 返回识别状态、条码内容与原始图片信息

    参数：
    - files: 用户上传的图片文件列表

    返回：
    - DecodeResponse：包含每张图片的识别结果与总体耗时
    """
    start_total = perf_counter()
    results: List[PerFileResult] = []

    for up in files:
        start_one = perf_counter()
        try:
            # 校验MIME类型（可根据需求扩展）
            if up.content_type not in {"image/png", "image/jpeg", "image/jpg"}:
                raise HTTPException(status_code=400, detail=f"不支持的图片类型: {up.content_type}")

            content = await up.read()
            size_bytes = len(content)

            # 并发友好：在线程池中执行CPU任务
            items: List[dict] = await anyio.to_thread.run_sync(decode_barcodes, content)
            barcodes = [BarcodeItem(**item) for item in items]

            # 图片信息
            width, height = await anyio.to_thread.run_sync(get_image_dimensions, content)
            image_info = ImageInfo(
                filename=up.filename,
                content_type=up.content_type,
                width=width,
                height=height,
                size_bytes=size_bytes,
            )

            elapsed_one = (perf_counter() - start_one) * 1000
            results.append(
                PerFileResult(
                    status="success" if barcodes else "failure",
                    barcodes=barcodes,
                    image=image_info,
                    error=None if barcodes else "未识别到一维码",
                    time_ms=round(elapsed_one, 2),
                )
            )

            logger.info(f"文件 {up.filename} 识别完成，条码数量={len(barcodes)}，耗时={elapsed_one:.2f}ms")
        except HTTPException as http_exc:
            elapsed_one = (perf_counter() - start_one) * 1000
            # 图片信息尽可能补充（若失败则置默认值）
            try:
                content = await up.read()
                width, height = await anyio.to_thread.run_sync(get_image_dimensions, content)
                size_bytes = len(content)
            except Exception:
                width, height, size_bytes = 0, 0, 0

            results.append(
                PerFileResult(
                    status="failure",
                    barcodes=[],
                    image=ImageInfo(
                        filename=up.filename,
                        content_type=up.content_type,
                        width=width,
                        height=height,
                        size_bytes=size_bytes,
                    ),
                    error=str(http_exc.detail),
                    time_ms=round(elapsed_one, 2),
                )
            )
            logger.warning(f"文件 {up.filename} 识别失败：{http_exc.detail}")
        except Exception as e:
            elapsed_one = (perf_counter() - start_one) * 1000
            results.append(
                PerFileResult(
                    status="failure",
                    barcodes=[],
                    image=ImageInfo(
                        filename=up.filename,
                        content_type=up.content_type,
                        width=0,
                        height=0,
                        size_bytes=0,
                    ),
                    error=f"服务器内部错误: {e}",
                    time_ms=round(elapsed_one, 2),
                )
            )
            logger.exception(f"文件 {up.filename} 处理异常：{e}")
        finally:
            try:
                await up.close()
            except Exception:
                pass

    total_ms = (perf_counter() - start_total) * 1000
    status = "success" if any(r.status == "success" for r in results) else "failure"
    return DecodeResponse(status=status, results=results, processing_time_ms=round(total_ms, 2))


if __name__ == "__main__":
    import uvicorn

    def start_server_background():
        """
        函数说明：在后台线程启动 uvicorn 服务，避免阻塞以便后续打开 WebView。

        行为：
        - 启动单进程 uvicorn，监听 127.0.0.1:8000。
        - 后台线程守护模式运行。
        """
        def _run():
            try:
                # 直接传递 app 对象，并禁用默认日志配置以兼容无控制台环境
                logger.info("Uvicorn 后台服务启动中…")
                uvicorn.run(app, host="0.0.0.0", port=8000, reload=False, log_level="info", log_config=None)
            except Exception as e:
                logger.exception(f"Uvicorn 启动失败：{e}")
        t = threading.Thread(target=_run, daemon=True)
        t.start()
        return t

    def wait_for_server(url: str = "http://127.0.0.1:8000/healthz", timeout: float = 15.0) -> bool:
        """
        函数说明：等待后端服务启动，轮询健康检查接口直到可用或超时。

        参数：
        - url: 健康检查地址
        - timeout: 最大等待秒数

        返回：
        - bool：服务是否在超时前可用
        """
        start = time.time()
        while time.time() - start < timeout:
            try:
                r = httpx.get(url, timeout=1.5)
                if r.status_code == 200:
                    return True
            except Exception:
                pass
            time.sleep(0.3)
        return False

    class JsApi:
        """
        函数说明：提供给前端调用的JS Bridge接口，用于执行系统剪贴板写入。

        方法：
        - copy_text(text): 将文本写入系统剪贴板，返回布尔表示成功与否。
        """
        def copy_text(self, text: str) -> bool:
            try:
                import win32clipboard as wc
                import win32con
                wc.OpenClipboard()
                wc.EmptyClipboard()
                wc.SetClipboardData(win32con.CF_UNICODETEXT, str(text))
                wc.CloseClipboard()
                return True
            except Exception as e:
                logger.exception(f"剪贴板复制失败：{e}")
                return False

    def launch_webview(url: str = "http://127.0.0.1:8000/ui") -> None:
        """
        函数说明：启动内置 WebView 指向前端页面，失败则回退到系统默认浏览器。

        参数：
        - url: 要打开的页面地址
        """
        try:
            import webview
            # 优先使用 EdgeChromium（Windows 10/11 支持 WebView2），不可用时由 pywebview 自动选择后端
            window = webview.create_window("一维码识别", url, width=1024, height=768, resizable=True, js_api=JsApi())
            try:
                webview.start(gui='edgechromium')
            except Exception:
                webview.start()
        except Exception:
            # 回退到系统默认浏览器
            webbrowser.open(url)

    # 1) 后台启动服务
    t = start_server_background()
    # 2) 等待服务可用
    if not wait_for_server():
        # 若服务未在预期时间内启动，也尝试打开页面，后端可能稍后就绪
        logger.warning("后端服务启动等待超时，将仍然尝试打开前端页面。")
    # 3) 打开内置 WebView（或回退到浏览器）
    launch_webview()